/*
 * Copyright 2022. Huawei Technologies Co., Ltd. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.huawei.jenkins.plugins.storage;

import com.cloudbees.plugins.credentials.BaseCredentials;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import com.cloudbees.plugins.credentials.matchers.IdMatcher;
import com.google.common.base.Preconditions;
import com.obs.services.ObsClient;
import com.obs.services.exception.ObsException;
import com.obs.services.model.*;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.model.Item;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import hudson.security.ACL;
import jenkins.MasterToSlaveFileCallable;
import jenkins.model.Jenkins;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Paths;
import java.util.*;

/**
 * obs处理
 */
public class ObsService implements Serializable {
    private static final long serialVersionUID = 1;
    public static StringCredentialsImpl getLoginInformation(String id) {
        if (StringUtils.isBlank(id)) {
            return null;
        }
        List<StringCredentialsImpl> credentials = CredentialsProvider.lookupCredentials(
                StringCredentialsImpl.class,  Jenkins.get(), ACL.SYSTEM, Collections.emptyList());
        IdMatcher matcher = new IdMatcher(id);
        for (StringCredentialsImpl c : credentials)
        {
            if (matcher.matches(c))
            {
                return c;
            }
        }
        return null;
    }

    /**
     * 按输入指定路径文件上传OBS
     *
     * @param envVars 环境变量
     * @param workspace 工作目录
     * @param listener 日志
     * @param customInput 用户输入
     * @return 上传结果
     * @throws IOException 异常
     * @throws InterruptedException 异常
     */

    public static String upload(EnvVars envVars, FilePath workspace, TaskListener listener, final CustomInput customInput) throws IOException, InterruptedException {
        inputValidate(customInput);

        final List<FilePath> children = new ArrayList<>();
        final FilePath dir;
        String workingDir = customInput.getWorkingDir();
        if (StringUtils.isNotBlank(workingDir)) {
            workingDir = envVars.expand(workingDir);
            if (workingDir.startsWith("/") || workingDir.endsWith("\\")) {
                workingDir = workingDir.substring(1);
            }
            dir = workspace.child(workingDir);
        } else {
            dir = workspace;
        }
        // file 目标文件
        // excludePathPattern & includePathPattern 正则匹配
        // 以上两种方式二选一，file优先级较高
        if (StringUtils.isNotBlank(customInput.getFile())) {
            children.add(dir.child(customInput.getFile()));
        } else if (StringUtils.isNotBlank(customInput.getExcludePathPattern()) || StringUtils.isNotBlank(customInput.getIncludePathPattern())) {
            children.addAll(Arrays.asList(dir.list(customInput.getIncludePathPattern(), customInput.getExcludePathPattern(), true)));
        } else {
            return "Nothing to upload";
        }
        if (children.isEmpty()) {
            listener.getLogger().println("Nothing to upload %n");
            return null;
        }
        String parentPath = "";
        if(customInput.getPath() != null){
            parentPath = envVars.expand(customInput.getPath());
        }

        int prefixLength = dir.getRemote().length() + 1;
        for (FilePath child : children) {
            String childRemotePath = child.getRemote();
            if (childRemotePath.length() <= prefixLength) {
                continue;
            }
            String fileName = child.getRemote().substring(prefixLength);
            if(parentPath.equals("")){
                fileName = String.join(File.separator, fileName).replace(File.separator, "/");
            }else {
                fileName = String.join(File.separator, parentPath, fileName).replace(File.separator, "/");
            }
            child.act(new RemoteUploader(listener, fileName, customInput));
            listener.getLogger().format("%s Upload complete %n", child.getName());
        }
        listener.getLogger().println("Upload complete %n");
        return String.format("obs://%s/%s", customInput.getBucket(), customInput.getPath());
    }

    private static void inputValidate(CustomInput customInput) {
        final String includePathPattern = customInput.getIncludePathPattern();
        final String file = customInput.getFile();
        final String ak = customInput.getAk();
        final String sk = customInput.getSk();
        //final String obsPath = customInput.getPath();
        //Preconditions.checkArgument(StringUtils.isNotBlank(obsPath), "ObsPath can not be blank");
        Preconditions.checkArgument(StringUtils.isNotBlank(file) || StringUtils.isNotBlank(includePathPattern), "At least one argument of Text, File or IncludePathPattern must be included");
        Preconditions.checkArgument(StringUtils.isBlank(includePathPattern) || StringUtils.isBlank(file), "File and IncludePathPattern cannot be used together");
        Preconditions.checkArgument(StringUtils.isNotBlank(ak) && StringUtils.isNotBlank(sk), "Ak, Sk can not be blank");
    }

    private static UploadFileRequest buildUploadFileRequest(CustomInput customInput, TaskListener listener) {
        Preconditions.checkArgument(StringUtils.isNotBlank(customInput.getBucket()), "Bucket must not be blank");
        ObjectMetadata metas = new ObjectMetadata();
        final Map<String, Object> metadataMap = new HashMap<String, Object>();
        if (ArrayUtils.isNotEmpty(customInput.getMetadatas())) {
            for (String metadata : customInput.getMetadatas()) {
                if (metadata.contains(":")) {
                    metadataMap.put(metadata.substring(0, metadata.indexOf(':')), metadata.substring(metadata.indexOf(':') + 1));
                }
            }
        }
        // Add metadata
        if (MapUtils.isNotEmpty(metadataMap)) {
            metas.setUserMetadata(metadataMap);
        }
        if (StringUtils.isNotBlank(customInput.getCacheControl())) {
            metas.setCacheControl(customInput.getCacheControl());
        }
        if (StringUtils.isNotBlank(customInput.getExpires())) {
            metas.setExpires(customInput.getExpires());
        }
        if (StringUtils.isNotBlank(customInput.getContentEncoding())) {
            metas.setContentEncoding(customInput.getContentEncoding());
        }
        if (StringUtils.isNotBlank(customInput.getContentType())) {
            metas.setContentType(customInput.getContentType());
        }
        if (StringUtils.isNotBlank(customInput.getContentDisposition())) {
            metas.setContentDisposition(customInput.getContentDisposition());
        }
        if (StringUtils.isNotBlank(customInput.getRedirectLocation())) {
            metas.setWebSiteRedirectLocation(customInput.getRedirectLocation());
        }
        if (StringUtils.isNotBlank(customInput.getRedirectLocation())) {
            metas.setWebSiteRedirectLocation(customInput.getRedirectLocation());
        }

        UploadFileRequest uploadFileRequest = new UploadFileRequest(customInput.getBucket(), customInput.getPath());
        uploadFileRequest.setObjectMetadata(metas);

        // Add acl
        if (StringUtils.isNotBlank(customInput.getAcl())) {
            uploadFileRequest.setAcl(getAcl(customInput.getAcl()));
        }

        // Add kms
        if (StringUtils.isNotBlank(customInput.getKmsId())) {
            listener.getLogger().format("Using KMS: %s%n", customInput.getKmsId());
            SseKmsHeader sseKmsHeader = new SseKmsHeader();
            sseKmsHeader.setKmsKeyId(customInput.getKmsId());
            uploadFileRequest.setSseKmsHeader(sseKmsHeader);
        }
        return uploadFileRequest;
    }

    private static AccessControlList getAcl(String acl) {
        switch (acl.toLowerCase()) {
            case "publicread":
                return AccessControlList.REST_CANNED_PUBLIC_READ;
            case "publicreadwrite":
                return AccessControlList.REST_CANNED_PUBLIC_READ_WRITE;
            case "publicreaddelivered":
                return AccessControlList.REST_CANNED_PUBLIC_READ_DELIVERED;
            case "publicreadwritedelivered":
                return AccessControlList.REST_CANNED_PUBLIC_READ_WRITE_DELIVERED;
        }
        return AccessControlList.REST_CANNED_PRIVATE;
    }


    public static String download(TaskListener listener,CustomInput customInput) {
        downloadInputValidate(customInput);
        ObsClient obsClient = new ObsClient(customInput.getAk(), customInput.getSk(), customInput.getEndpoint());
        List<String> failedList = new ArrayList<>();

        ListObjectsRequest listRequest = new ListObjectsRequest();
        listRequest.setPrefix(customInput.getRemotePrefix());
        listRequest.setBucketName(customInput.getBucket());
        listRequest.setEncodingType("url");
        ObjectListing objects;

        for (int i = 1; ; i++) {
            listener.getLogger().println("Start to Download page " + i);
            try {
                objects = obsClient.listObjects(listRequest);
            } catch (ObsException e) {
                listener.getLogger().println("Request Error, RequestID is: " + e.getErrorRequestId());
                throw e;
            }

            for (ObsObject object: objects.getObjects()) {
                if(object.getObjectKey().endsWith("/")){
                    continue;
                }
                DownloadFileRequest downloadRequest = new DownloadFileRequest(customInput.getBucket(), object.getObjectKey());
                // 将对象名转换为本地路径
                String downloadPath = Paths.get(customInput.getLocalFolder(),
                        object.getObjectKey().replace("/", File.separator)).toString();

                downloadRequest.setDownloadFile(downloadPath);
                // 设置并发下载数为 10
                downloadRequest.setTaskNum(10);
                try {
                    listener.getLogger().println("Start to download object [" + object.getObjectKey()
                            + "] to [" + downloadPath + "]");
                    obsClient.downloadFile(downloadRequest);
                } catch (ObsException e) {
                    listener.getLogger().println("Failed to download " + object.getObjectKey()
                            + " RequestID: " + e.getErrorRequestId());
                    failedList.add(object.getObjectKey());
                } catch (Exception e) {
                    failedList.add(object.getObjectKey());
                }
            }
            if (!objects.isTruncated()) {
                break;
            }
            // 使用上次返回的 next_marker 作为下次列举的 marker
            listRequest.setMarker(objects.getNextMarker());
        }
        failedList.forEach(item -> listener.getLogger().println("Failed to download " + item + ", please try again"));
        return "success";
    }

    private static void downloadInputValidate(CustomInput customInput) {
        final String ak = customInput.getAk();
        final String sk = customInput.getSk();
        final String bucketName = customInput.getBucket();
        final String endpoint = customInput.getEndpoint();
        final String localFolder = customInput.getLocalFolder();

        Preconditions.checkArgument(StringUtils.isNotBlank(localFolder), "local Folder path can not be blank");
        Preconditions.checkArgument(StringUtils.isNotBlank(endpoint), "Obs endpoint can not be blank");
        Preconditions.checkArgument(StringUtils.isNotBlank(bucketName), "Obs bucketName can not be blank");
        Preconditions.checkArgument(StringUtils.isNotBlank(ak) && StringUtils.isNotBlank(sk), "Ak, Sk can not be blank");
    }



    private static class RemoteUploader extends MasterToSlaveFileCallable<Void> {

        protected static final long serialVersionUID = 1L;

        private final TaskListener taskListener;

        private final String fileName;

        private final CustomInput customInput;

        RemoteUploader(TaskListener taskListener, String fileName, CustomInput customInput) {
            this.taskListener = taskListener;
            this.fileName = fileName;
            this.customInput = customInput;
        }

        @Override
        public Void invoke(File localFile, VirtualChannel channel) throws IOException, InterruptedException {
            customInput.setPath(fileName);
            UploadFileRequest uploadFileRequest = buildUploadFileRequest(customInput, taskListener);
            String path = uploadFileRequest.getObjectKey();
            taskListener.getLogger().format("Uploading file to obs://%s/%s %n", uploadFileRequest.getBucketName(), path);
            ObsClient obsClient = new ObsClient(customInput.getAk(), customInput.getSk(), customInput.getEndpoint());
            try {
                uploadFileRequest.setUploadFile(localFile.getAbsolutePath());
                obsClient.uploadFile(uploadFileRequest);
            } finally {
                if (obsClient != null) {
                    obsClient.close();
                }
            }
            return null;
        }
    }
}
